/*
 * Copyright 2009 Jamie Gennis
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.google.code.gwt.remoteaction.rebind;

import com.google.code.gwt.remoteaction.client.AbstractActionProxy;
import com.google.code.gwt.remoteaction.client.Action;
import com.google.code.gwt.remoteaction.client.ActionResult;
import com.google.code.gwt.remoteaction.client.IllegalActionResultException;
import com.google.code.gwt.remoteaction.client.RemoteAction;
import com.google.code.gwt.remoteaction.server.Utils;
import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.impl.ArtificialRescue;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.rpc.rebind.RpcProxyCreator;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * A class for creating a remote action service proxy class for a given
 * RemoteActionService interface.
 *
 * @author jgennis
 */
public class RemoteActionProxyCreator {

  protected static class ActionResultInfo {

    protected final String canonicalClassName;

    protected final String rescue;

    public ActionResultInfo(String canonicalClassName, String rescue) {
      this.canonicalClassName = canonicalClassName;
      this.rescue = rescue;
    }

    public String getCanonicalClassName() {
      return canonicalClassName;
    }

    public String getRescue() {
      return rescue;
    }
  }

  protected JClassType serviceInterface;

  protected JClassType serviceAsyncInterface;

  public RemoteActionProxyCreator(JClassType serviceInterface) {
    if (serviceInterface == null)
      throw new IllegalArgumentException("The 'remoteActionServiceType' argument may not be null");
    this.serviceInterface = serviceInterface;
  }

  public String create(TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();
    serviceAsyncInterface = typeOracle.findType(serviceInterface.getQualifiedSourceName() +
        "Async");

    generateDummyRpcService(logger, context);

    Map<JMethod, ActionResultInfo> methodResultInfo = generateActionResults(logger, context);

    SourceWriter srcWriter = getSourceWriter(logger, context, methodResultInfo.values());
    if (srcWriter == null)
      return getProxyCanonicalName();

    generateProxyMethods(logger, context, srcWriter, methodResultInfo);

    srcWriter.commit(logger);

    return getProxyCanonicalName();
  }

  /**
   * Generate the RpcService proxy implementation so that the return types and
   * throwable types for the service interface are all deserializable.
   * 
   * @param logger the tree logger for this operation
   * @param context the generator context
   * @throws UnableToCompleteException if the RpcService proxy cannot be generated
   */
  protected void generateDummyRpcService(TreeLogger logger, GeneratorContext context)
      throws UnableToCompleteException {
    TreeLogger dummyLogger = logger.branch(TreeLogger.Type.DEBUG, "Creating dummy RpcService " +
        "proxy class");
    RpcProxyCreator creator = new RpcProxyCreator(serviceInterface);
    creator.create(dummyLogger, context);
  }

  protected Map<JMethod, ActionResultInfo> generateActionResults(TreeLogger logger,
      GeneratorContext context) throws UnableToCompleteException {
    TypeOracle typeOracle = context.getTypeOracle();
    Map<JMethod, ActionResultInfo> toReturn = new HashMap<JMethod, ActionResultInfo>();

    for (JMethod method : serviceInterface.getMethods()) {
      JClassType actionType = getActionType(method, typeOracle);
      JType returnType = method.getReturnType();
      JClassType resultType;
      if (returnType.isPrimitive() != null) {
        resultType = typeOracle.findType(returnType.isPrimitive().getQualifiedBoxedSourceName());
        assert resultType != null : "no boxed type found for primitive type " +
            returnType.getQualifiedSourceName();
      } else {
        resultType = returnType.isClassOrInterface();
        assert resultType != null : "non-primitive type " + returnType.getQualifiedSourceName() +
            " is neither a class nor an interface";
      }
      Set<JClassType> throwableTypes = new HashSet<JClassType>();
      for (JType throwableType : method.getThrows()) {
        JClassType throwableClassType = throwableType.isClass();
        assert throwableClassType != null : "thrown type is not a class";
        throwableTypes.add(throwableClassType);
      }

      ActionResultCreator creator = new ActionResultCreator(actionType, resultType,
          throwableTypes);
      TreeLogger actionResultLogger = logger.branch(TreeLogger.Type.DEBUG, "Creating action " +
          "result class for remote action method " + serviceInterface.getQualifiedSourceName() +
          "#" + method.getReadableDeclaration());
      String actionResultName = creator.create(actionResultLogger, context);
      String actionResultRescue = creator.getRescue();
      ActionResultInfo actionResultInfo = new ActionResultInfo(actionResultName,
          actionResultRescue);
      toReturn.put(method, actionResultInfo);
    }
    return toReturn;
  }

  protected void generateProxyMethods(TreeLogger logger, GeneratorContext context,
      SourceWriter srcWriter, Map<JMethod, ActionResultInfo> methodResultInfo) {
    JMethod[] asyncMethods = serviceAsyncInterface.getMethods();
    for (JMethod asyncMethod : asyncMethods) {
      JMethod syncMethod = getSyncMethod(asyncMethod);
      TreeLogger methodLogger = logger.branch(TreeLogger.Type.DEBUG, "Generating proxy method '" +
          asyncMethod.getReadableDeclaration());

      ActionResultInfo actionResultInfo = methodResultInfo.get(syncMethod);
      assert actionResultInfo != null : "could not get action result info for method " +
          syncMethod.getReadableDeclaration();
      String actionResultName = actionResultInfo.getCanonicalClassName();
      generateProxyMethod(methodLogger, context, srcWriter, asyncMethod, actionResultName);
    }
  }

  protected void generateProxyMethod(TreeLogger logger, GeneratorContext context,
      SourceWriter srcWriter, JMethod asyncMethod, String actionResultName) {
    TypeOracle typeOracle = context.getTypeOracle();
    JMethod syncMethod = getSyncMethod(asyncMethod);
    JClassType actionType = getActionType(syncMethod, typeOracle);

    srcWriter.println();
    srcWriter.println("public void " + asyncMethod.getName() + "(");
    srcWriter.indent();
    srcWriter.indent();
    JParameter[] parameters = asyncMethod.getParameters();
    JParameter callbackParam = parameters[parameters.length - 1];
    for (int i = 0; i < parameters.length - 1; ++i) {
      JParameter param = parameters[i];
      srcWriter.println("final " + param.getType().getParameterizedQualifiedSourceName() + " " +
          param.getName() + ",");
    }
    srcWriter.println("final " + callbackParam.getType().getParameterizedQualifiedSourceName() +
        " callback) {");
    srcWriter.outdent();

    // foo.Bar action = new foo.Bar(...);
    String actionTypeName = actionType.getParameterizedQualifiedSourceName();
    srcWriter.print("Action action = new " + actionTypeName + "(");
    srcWriter.indent();
    srcWriter.indent();
    for (int i = 0; i < parameters.length - 1; ++i) {
      String end = (i < parameters.length - 2) ? "," : "";
      srcWriter.println();
      srcWriter.print(parameters[i].getName() + end);
    }
    srcWriter.println(");");
    srcWriter.outdent();
    srcWriter.outdent();

    // AsyncCallback<ActionResult> callbackWrapper = new AsyncCallback<foo.SomeActionResult>() {
    srcWriter.println();
    srcWriter.println("AsyncCallback<ActionResult> callbackWrapper" + "= new " +
        "AsyncCallback<ActionResult>() {");
    srcWriter.indent();

    /*
     *   @Override
     *   public void onFailure(Throwable caught) {
     *     callback.onFailure(caught);
     *   }
     */
    srcWriter.println();
    srcWriter.println("@Override");
    srcWriter.println("public void onFailure(Throwable caught) {");
    srcWriter.indentln("callback.onFailure(caught);");
    srcWriter.println("}");

    /*
     *   @Override
     *   public void onSuccess(foo.bar.SomeActionResult actionResult) {
     *     if (!(actionResult instanceof ActionResultFoo)) {
     *       Exception ex = new IllegalActionResultException("...", actionResult);
     *       callback.onFailure(ex);
     *       return;
     *     }
     *     ActionResultFoo validActionResult = (ActionResultFoo) actionResult;
     *     Throwable caught = validActionResult.getException();
     *     if (caught != null)
     *       callback.onFailure(caught);
     *     else
     *       callback.onSuccess(validActionResult.getResult());
     *   }
     * };
     */
    srcWriter.println();
    srcWriter.println("@Override");
    srcWriter.println("public void onSuccess(ActionResult actionResult) {");
    srcWriter.indent();
    srcWriter.println("if (!(actionResult instanceof " + actionResultName + ")) {");
    srcWriter.indent();
    srcWriter.println("Exception ex = new IllegalActionResultException(\"The ActionResult " +
        "instance that resulted from the execution of the " + syncMethod.toString() + " action " +
        "method was not of the correct type.  This is likely a programming error in one of the " +
        "action filters.\", actionResult);");
    srcWriter.println("callback.onFailure(ex);");
    srcWriter.println("return;");
    srcWriter.outdent();
    srcWriter.println("}");

    srcWriter.println(actionResultName + " validActionResult = (" + actionResultName +
        ") actionResult;");
    srcWriter.println("Throwable thrown = validActionResult.getException();");
    srcWriter.println("if (thrown != null)");
    srcWriter.indentln("callback.onFailure(thrown);");
    srcWriter.println("else");
    srcWriter.indentln("callback.onSuccess(validActionResult.getResult());");
    srcWriter.outdent();
    srcWriter.println("}");
    srcWriter.outdent();
    srcWriter.println("};");

    // proxyExecution(action, callbackWrapper);
    srcWriter.println();
    srcWriter.println("proxyExecution(action, callbackWrapper);");

    srcWriter.outdent();
    srcWriter.println("}");
  }

  protected SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext context,
      Collection<ActionResultInfo> actionResultInfos) {
    PrintWriter printWriter = context.tryCreate(logger, Utils.INTERNAL_PACKAGE,
        getProxySimpleName());
    if (printWriter == null) {
      return null;
    }

    ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(
        Utils.INTERNAL_PACKAGE, getProxySimpleName());

    String[] imports = new String[]{
      GWT.class.getCanonicalName(),
      AsyncCallback.class.getCanonicalName(),
      ArtificialRescue.class.getCanonicalName(),
      ArtificialRescue.Rescue.class.getCanonicalName(),
      Action.class.getCanonicalName(),
      ActionResult.class.getCanonicalName(),
      IllegalActionResultException.class.getCanonicalName(),
      AbstractActionProxy.class.getCanonicalName(),
      serviceAsyncInterface.getQualifiedSourceName()
    };
    for (String imp : imports)
      composerFactory.addImport(imp);

    /*
     * Having an empty @ArtificialRescue({}) annotation causes a
     * NullPointerException in the ArtificialRescueChecker class.
     */
    if (!actionResultInfos.isEmpty()) {
      String artificialRescue = getArtificialRescue(actionResultInfos);
      composerFactory.addAnnotationDeclaration(artificialRescue);
    }

    composerFactory.setSuperclass(AbstractActionProxy.class.getSimpleName());
    composerFactory.addImplementedInterface(serviceAsyncInterface.getName());

    return composerFactory.createSourceWriter(context, printWriter);
  }

  protected JMethod getSyncMethod(JMethod asyncMethod) {
    JParameter[] asyncParams = asyncMethod.getParameters();
    JType[] syncParamTypes = new JType[asyncParams.length - 1];
    for (int i = 0; i < syncParamTypes.length; ++i)
      syncParamTypes[i] = asyncParams[i].getType();
    JMethod syncMethod = serviceInterface.findMethod(asyncMethod.getName(), syncParamTypes);
    return syncMethod;
  }

  protected JClassType getActionType(JMethod method, TypeOracle typeOracle) {
    RemoteAction remoteAction = method.getAnnotation(RemoteAction.class);
    JClassType actionType = typeOracle.findType(remoteAction.value().getCanonicalName());
    return actionType;
  }

  protected String getProxyCanonicalName() {
    return Utils.getProxyCanonicalClassName(serviceInterface.getQualifiedSourceName());
  }

  protected String getProxySimpleName() {
    return Utils.getProxySimpleClassName(serviceInterface.getQualifiedSourceName());
  }

  protected String getArtificialRescue(Collection<ActionResultInfo> actionResultInfos) {
    StringBuilder sb = new StringBuilder();
    sb.append("@ArtificialRescue({");

    /*
     * Rescue the GWT RPC proxy's type overrides factory to take advantage of
     * the rescues that it does.
     *
     * XXX: Currently there's no good way to get the name of the
     * TypeOverridesFactory, so we just divine the name based on what GWT
     * happens to do now.
     */
    String typeOverrideName = serviceInterface.getQualifiedSourceName() + "_TypeOverridesFactory";
    sb.append("@Rescue(\n").
        append("    className = \"").append(typeOverrideName).append("\",\n").
        append("    instantiable = true)");

    // Rescue the ActionResult classes
    for (ActionResultInfo actionResultInfo : actionResultInfos) {
      sb.append(",\n    ").append(actionResultInfo.getRescue());
    }
    sb.append("})");
    return sb.toString();
  }
}
